Инвесторы задумали расширить сферу своей деятельности и открыть в Москве заведение общественного питания. Для этого им необходимо исследование рынка Москвы, чтобы понять, где лучше всего расположить кофейню и какие цены будут оптимальными. Также им потребуется презентация, в которой будут изложены основные аспекты исследования.
Подготовить исследование рынка общественного питания Москвы, найти интересные особенности и презентовать полученные результаты, которые в будущем помогут в выборе подходящего инвесторам места для открытия заведения общественного питания.
Доступен датасет(moscow_places.csv) с заведениями общественного питания Москвы, составленный на основе данных сервисов Яндекс Карты и Яндекс Бизнес на лето 2022 года.
Описание столбцов в датасете moscow_places.csv:
Установим необходимые библиотеки
#импорт библиотек
import pandas as pd
import numpy as np
import scipy.stats as st
import datetime as dt
from IPython.display import display
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
from plotly import graph_objects as go
from plotly.subplots import make_subplots
import folium
from folium import Map, Marker, Choropleth, PolyLine
from folium.plugins import MarkerCluster
from folium.features import CustomIcon
import geopy.distance
from geojson import Polygon, Feature, FeatureCollection
#Чтение датасета
try:
data = pd.read_csv('datasets/moscow_places.csv')
except:
data = pd.read_csv('https://code.s3.yandex.net/datasets/moscow_places.csv')
pd.options.mode.chained_assignment = None
#Создаем функцию для изучения датасета, преведения названий строк к змеиному регистру и подсчету полных дубликатов
def data_info(data):
data.columns = [(x.lower()).replace(' ', '_') for x in data.columns]
print('-' * 80)
display(data.head())
print('-' * 80)
data.info()
print('-' * 80)
print('Кол-во полных дубликатов в таблице:', data.duplicated().sum())
print('-' * 80)
#Создадим функцию для просмотра максимальной и минимальной дате в столбцах
def data_dt_info(data, dt):
print('Минимальная дата в столбце', dt, data[dt].min(),
'\nМаксимальная дата в столбце', dt, data[dt].max())
#Создаю переменные, где будет хранится первоначально число строк и уникальных пользователей
count_entries = data.shape[0]
#Создаем функцию, которая будет выводить, сколько было удаленно строк и уникальных пользователей
def difference_of_values():
print(f'Всего удаленно строк {count_entries - data.shape[0]} \
или {round(100 * (count_entries - data.shape[0]) / count_entries, 2)}% от первоначального кол-во строк')
#изучаем датасет
data_info(data)
--------------------------------------------------------------------------------
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | 0 | NaN |
| 1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | 0 | 4.0 |
| 2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | 0 | 45.0 |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | 0 | NaN |
| 4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | 1 | 148.0 |
-------------------------------------------------------------------------------- <class 'pandas.core.frame.DataFrame'> RangeIndex: 8406 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8406 non-null object 1 category 8406 non-null object 2 address 8406 non-null object 3 district 8406 non-null object 4 hours 7870 non-null object 5 lat 8406 non-null float64 6 lng 8406 non-null float64 7 rating 8406 non-null float64 8 price 3315 non-null object 9 avg_bill 3816 non-null object 10 middle_avg_bill 3149 non-null float64 11 middle_coffee_cup 535 non-null float64 12 chain 8406 non-null int64 13 seats 4795 non-null float64 dtypes: float64(6), int64(1), object(7) memory usage: 919.5+ KB -------------------------------------------------------------------------------- Кол-во полных дубликатов в таблице: 0 --------------------------------------------------------------------------------
#Посмотрим какие есть категории заведений
data.category.value_counts().reset_index()
| index | category | |
|---|---|---|
| 0 | кафе | 2378 |
| 1 | ресторан | 2043 |
| 2 | кофейня | 1413 |
| 3 | бар,паб | 765 |
| 4 | пиццерия | 633 |
| 5 | быстрое питание | 603 |
| 6 | столовая | 315 |
| 7 | булочная | 256 |
В данных представлено 8406 заведений из 8 категорий.
#Приведем названия к нижнему регистру
data.name = data.name.str.lower()
#Так как, полные дубликаты отсутсвуют, посмотрим есть ли полные дубликаты среди первых 4-х столбцов
print('Кол-во полных дубликатов среди первых 4-х столбцов:', data.iloc[:,:4].duplicated().sum())
Кол-во полных дубликатов среди первых 4-х столбцов: 1
#Уберем дубликат
data = data.drop(data[data.iloc[:,:4].duplicated()].index).reset_index()
#Посмотрим описательную статистику по данным
data.describe()
| index | lat | lng | rating | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|
| count | 8405.000000 | 8405.000000 | 8405.000000 | 8405.000000 | 3149.000000 | 535.000000 | 8405.000000 | 4794.000000 |
| mean | 4202.820226 | 55.750102 | 37.608584 | 4.229899 | 958.053668 | 174.721495 | 0.381202 | 108.405090 |
| std | 2426.714272 | 0.069659 | 0.098595 | 0.470376 | 1009.732845 | 88.951103 | 0.485711 | 122.840831 |
| min | 0.000000 | 55.573942 | 37.355651 | 1.000000 | 0.000000 | 60.000000 | 0.000000 | 0.000000 |
| 25% | 2102.000000 | 55.705155 | 37.538626 | 4.100000 | 375.000000 | 124.500000 | 0.000000 | 40.000000 |
| 50% | 4203.000000 | 55.753407 | 37.605260 | 4.300000 | 750.000000 | 169.000000 | 0.000000 | 75.000000 |
| 75% | 6304.000000 | 55.795033 | 37.664793 | 4.400000 | 1250.000000 | 225.000000 | 1.000000 | 140.000000 |
| max | 8405.000000 | 55.928943 | 37.874466 | 5.000000 | 35000.000000 | 1568.000000 | 1.000000 | 1288.000000 |
Видим, очень странный минимальный средний счет, а именно 0 рублей, такого быть не может, изучим подробнее. Также видим очень высокую стоимость чашки капучино(1568 рублей) и высокий средний счет(35000 рублей)
#Выведим все заведения, где счет менее 100 рублей или более 10 000 или стоимость чашки капучино более 400
data.query('middle_avg_bill < 100 | middle_coffee_cup > 400 | middle_avg_bill > 10000')
| index | name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 730 | 730 | чойхона | бар,паб | Москва, Дмитровское шоссе, 95А | Северный административный округ | ежедневно, 10:00–23:00 | 55.871497 | 37.543555 | 4.4 | высокие | Средний счёт:5000–17000 ₽ | 11000.0 | NaN | 0 | 49.0 |
| 1950 | 1951 | пончики! выпекаем на месте | кофейня | Москва, улица Яблочкова, 19Г | Северо-Восточный административный округ | ежедневно, 08:00–22:00 | 55.819120 | 37.578073 | 4.6 | NaN | Средний счёт:от 50 ₽ | 50.0 | NaN | 1 | 250.0 |
| 2315 | 2316 | монастырская чайная | кафе | Москва, 2-й Красносельский переулок, 5с3 | Центральный административный округ | ежедневно, 09:00–21:00 | 55.783773 | 37.666269 | 4.1 | NaN | Средний счёт:35–100 ₽ | 67.0 | NaN | 0 | NaN |
| 2858 | 2859 | шоколадница | кофейня | Москва, Большая Семёновская улица, 27, корп. 1 | Восточный административный округ | ежедневно, 08:00–23:00 | 55.782268 | 37.709022 | 4.2 | средние | Цена чашки капучино:230–2907 ₽ | NaN | 1568.0 | 1 | 48.0 |
| 3687 | 3688 | кофемания | кофейня | Москва, улица Новый Арбат, 19 | Центральный административный округ | ежедневно, круглосуточно | 55.752136 | 37.587784 | 4.5 | высокие | Средний счёт:от 0 ₽ | 0.0 | NaN | 1 | 200.0 |
| 5419 | 5420 | пончики! выпекаем на месте | булочная | Москва, Новокосинская улица, 35 | Восточный административный округ | ежедневно, 08:00–22:00 | 55.743570 | 37.866127 | 4.3 | NaN | Средний счёт:от 50 ₽ | 50.0 | NaN | 1 | 4.0 |
| 6427 | 6428 | бургер mix | быстрое питание | Москва, улица Гурьянова, 2А | Юго-Восточный административный округ | ежедневно, 10:00–21:00 | 55.693587 | 37.722745 | 4.1 | низкие | Средний счёт:90 ₽ | 90.0 | NaN | 0 | 40.0 |
| 6542 | 6543 | пончики! выпекаем на месте | кофейня | Москва, проспект Вернадского, 109 | Западный административный округ | пн-сб 08:00–22:00; вс 09:00–22:00 | 55.662333 | 37.483580 | 4.7 | NaN | Средний счёт:от 50 ₽ | 50.0 | NaN | 1 | 5.0 |
| 7176 | 7177 | кафе | ресторан | Москва, Каширское шоссе, 23, стр. 2 | Южный административный округ | ежедневно, круглосуточно | 55.657450 | 37.646665 | 4.1 | высокие | Средний счёт:20000–50000 ₽ | 35000.0 | NaN | 0 | 100.0 |
| 7296 | 7297 | пончики! выпекаем на месте | кофейня | Москва, Люблинская улица, 163/1 | Юго-Восточный административный округ | ежедневно, 08:00–22:00 | 55.651354 | 37.744222 | 4.7 | NaN | Средний счёт:50 ₽ | 50.0 | NaN | 1 | 93.0 |
| 7544 | 7545 | шаурмян у арена | быстрое питание | Москва, Краснодарская улица, 47 | Юго-Восточный административный округ | ежедневно, 09:00–23:00 | 55.677744 | 37.764810 | 3.3 | NaN | Средний счёт:от 30 ₽ | 30.0 | NaN | 0 | 2.0 |
В кофемании на новом Арбате средний счет не может быть от 0 рублей, а в кафе на Каширском шоссе средний счет не может быть 35 000 рублей, так как это кафе при больнице, можно сказать столовая. Также стоимость чашки капучино в Шоколаднице на Большой Семёновской не может быть 1568 рублей. Остальные данные похожи на реальность. Заполним столбец среднего счета и стоимости чашки капучино пропуском.
#Заполним столбец среднего счета и стоимости чашки капучино пропуском
data.loc[data.middle_avg_bill == 0, ['avg_bill', 'middle_avg_bill']] = np.nan
data.loc[data.middle_avg_bill > 10000, ['avg_bill', 'middle_avg_bill']] = np.nan
data.loc[data.middle_coffee_cup > 400, ['avg_bill', 'middle_coffee_cup']] = np.nan
#Проверим, что параметр chain выставлен верно, посмотрим на несетевые заведения с одинаковыми названиями
data.query('chain == 0').groupby('name', as_index=False).chain.count() \
.query('chain > 1').sort_values('chain', ascending=False).head(20)
| name | chain | |
|---|---|---|
| 2468 | кафе | 189 |
| 4640 | шаурма | 43 |
| 3642 | ресторан | 34 |
| 3990 | столовая | 28 |
| 2768 | кофейня | 12 |
| 1753 | бистро | 12 |
| 2611 | кафе-столовая | 9 |
| 1826 | буфет | 8 |
| 4146 | трапезная | 7 |
| 4689 | шашлычная | 6 |
| 3548 | поминальные обеды | 5 |
| 3499 | пиццерия | 3 |
| 1769 | блины | 3 |
| 4124 | токио рамен | 2 |
| 4439 | центр плова | 2 |
| 4678 | шашлык на углях | 2 |
| 2994 | ма ми | 2 |
| 3074 | между булок | 2 |
| 3226 | на углях | 2 |
| 4612 | чудо тандыр | 2 |
Для всех несетевых заведений с одинаковым названием верно, указан параметр chain == 0
#Посмотрим, сколько пропусков, в каждом из столбцов и в каком соотношение
data_gaps = data.isna().sum().reset_index().rename(columns={0: 'gaps'})
data_gaps['procent'] = round(data_gaps.gaps / data.shape[0] * 100, 2)
print('Всего пропусков:', data_gaps.gaps.sum())
data_gaps.sort_values('procent')
Всего пропусков: 26960
| index | gaps | procent | |
|---|---|---|---|
| 0 | index | 0 | 0.00 |
| 1 | name | 0 | 0.00 |
| 2 | category | 0 | 0.00 |
| 3 | address | 0 | 0.00 |
| 4 | district | 0 | 0.00 |
| 6 | lat | 0 | 0.00 |
| 7 | lng | 0 | 0.00 |
| 8 | rating | 0 | 0.00 |
| 13 | chain | 0 | 0.00 |
| 5 | hours | 536 | 6.38 |
| 14 | seats | 3611 | 42.96 |
| 10 | avg_bill | 4593 | 54.65 |
| 9 | price | 5090 | 60.56 |
| 11 | middle_avg_bill | 5259 | 62.57 |
| 12 | middle_coffee_cup | 7871 | 93.65 |
Мы видим, что пропуски присутствуют в 6 столбцах:
Большинство столбцов с пропусками имеют более 50% пропусков. Всего около 27 тысяч пропусков. Далее попробуем восстановить данные.
#Проверим, есть ли в средней стоимости заказа, информация о среднем счете или стоимости чашки кофе и наоборот
print(data.query('avg_bill.isna() & (middle_avg_bill.notna() | middle_coffee_cup.notna())').shape[0])
print(data.query('avg_bill.str.contains("чашки") & middle_coffee_cup.isna()').shape[0])
print(data.query('avg_bill.str.contains("счёт") & middle_avg_bill.isna()').shape[0])
0 0 0
#Посмотрим есть ли повторяются ли имена сетевых заведений в не сетевых заведений
data.query('chain == 1').name.isin(data.query('chain == 0').name.unique()).sum()
260
Так как названия сетевых заведения, с параметром chain == 1, не повторяются с обычными названиями, то мы можем для сетевых заведений найти пропущенные данные, так как в большинстве сетевых заведениях цены, средние чеки и время работы совпадают.
Создадим функцию для заполнения пропусков сетевых заведений с одинаковыми параметрами, а именно название и район. По названию и/или району будем группировать данные и пересечение только с 2 уникальными значениями(NaN и известное) будем использовать для заполнения пропусков.
#Создаем функцию
def chain_fill(data=data, column_fill='price', fill_nan_values='a', columns_to_group=['name'], max_values=2):
#Выводим первоначальное кол-во пропусков в столбце, который заполняем
print('Было незаполненых строк:', data[column_fill].isna().sum())
#Создаем сгрупирированную таблицу сетевых заведений по указанным столбцам,
#куда заносим все уникальные значения заполняемого столбца и кол-во пересечений
data_fill = data.fillna(fill_nan_values).query('chain == 1'). \
groupby(columns_to_group).agg({column_fill: ['unique', 'count']}).reset_index()
#Переименуем столбцы для более удобной работы
data_fill.columns = columns_to_group + column_fill.split() + ['count']
#Оставим только пересечения, где кол-во уникальных значений равно 2
data_fill['len'] = data_fill[column_fill].apply(lambda x: len(x));
data_fill = data_fill.query('len <= @max_values & len > 1')
#Вынесем уникальные значения в 2 отдельных столбцы
data_fill[column_fill] = data_fill[column_fill].apply(lambda x: sorted(x))
data_fill['null'] = data_fill[column_fill].apply(lambda x: x[0]);
data_fill['first'] = data_fill[column_fill].apply(lambda x: x[1]);
#Оставим только пересечение, где первое уникальное значение неизвестное, а второе известное
data_fill = data_fill.query('null == @fill_nan_values & first != @fill_nan_values')
#Соеденим исходную таблицу со сгруппированной таблицей
data_final = data.merge(data_fill[columns_to_group + ['first']], how='left', on=columns_to_group)
#Заполним пропуски в исходной таблицу и удалим лишний столбец
data_final[column_fill] = data_final[column_fill].fillna(data_final['first'])
data_final = data_final.drop('first', axis=1)
#Выводим кол-во пропусков в столбце, который заполняем после заполнения
print('Стола незаполненых строк:', data_final[column_fill].isna().sum())
return data_final
#Воспользуемся функцией и заполним пропуски в сетевых заведениях,
#сначала по пересечениям название+район, потом только по названию
print('Колонка hours:')
data = chain_fill(data=data, column_fill='hours', fill_nan_values='a',
columns_to_group=['name', 'district'], max_values=2)
data = chain_fill(data=data, column_fill='hours', fill_nan_values='a',
columns_to_group=['name'], max_values=2)
print('-'*80)
print('Колонка price:')
data = chain_fill(data=data, column_fill='price', fill_nan_values='a',
columns_to_group=['name', 'district'], max_values=2)
data = chain_fill(data=data, column_fill='price', fill_nan_values='a',
columns_to_group=['name'], max_values=2)
print('-'*80)
print('Колонка avg_bill:')
data = chain_fill(data=data, column_fill='avg_bill', fill_nan_values='a',
columns_to_group=['name', 'district'], max_values=2)
data = chain_fill(data=data, column_fill='avg_bill', fill_nan_values='a',
columns_to_group=['name'], max_values=2)
print('-'*80)
print('Колонка middle_avg_bill:')
data = chain_fill(data=data, column_fill='middle_avg_bill', fill_nan_values=0,
columns_to_group=['name', 'district'], max_values=2)
data = chain_fill(data=data, column_fill='middle_avg_bill', fill_nan_values=0,
columns_to_group=['name'], max_values=2)
print('-'*80)
print('Колонка middle_coffee_cup:')
data = chain_fill(data=data, column_fill='middle_coffee_cup', fill_nan_values=0,
columns_to_group=['name', 'district'], max_values=2)
data = chain_fill(data=data, column_fill='middle_coffee_cup', fill_nan_values=0,
columns_to_group=['name'], max_values=2)
print('-'*80)
print('Колонка seats:')
data = chain_fill(data=data, column_fill='seats', fill_nan_values=0,
columns_to_group=['name', 'district'], max_values=2)
data = chain_fill(data=data, column_fill='seats', fill_nan_values=0,
columns_to_group=['name'], max_values=2)
print('-'*80)
#Посмотрим, сколько пропусков осталось
print('Всего пропусков:', data.isna().sum().sum())
Колонка hours: Было незаполненых строк: 536 Стола незаполненых строк: 515 Было незаполненых строк: 515 Стола незаполненых строк: 487 -------------------------------------------------------------------------------- Колонка price: Было незаполненых строк: 5090 Стола незаполненых строк: 4614 Было незаполненых строк: 4614 Стола незаполненых строк: 4173 -------------------------------------------------------------------------------- Колонка avg_bill: Было незаполненых строк: 4593 Стола незаполненых строк: 4325 Было незаполненых строк: 4325 Стола незаполненых строк: 4086 -------------------------------------------------------------------------------- Колонка middle_avg_bill: Было незаполненых строк: 5259 Стола незаполненых строк: 5007 Было незаполненых строк: 5007 Стола незаполненых строк: 4773 -------------------------------------------------------------------------------- Колонка middle_coffee_cup: Было незаполненых строк: 7871 Стола незаполненых строк: 7768 Было незаполненых строк: 7768 Стола незаполненых строк: 7722 -------------------------------------------------------------------------------- Колонка seats: Было незаполненых строк: 3611 Стола незаполненых строк: 3396 Было незаполненых строк: 3396 Стола незаполненых строк: 3165 -------------------------------------------------------------------------------- Всего пропусков: 24406
После заполнение пропусков, общее кол-во пропусков уменьшилось примерно на ~2,5 тысячи
Добавим отдельный столбец с названием улицы. Исключением станут адресса с МКАД, для них мы добавим название дороги и номер километра.
#Добавляем новый столбец и заполняем его улицами
data['street'] = data.address.str.split(',', expand=True)[1]
#Для адрессов с МКАД, добавим название дороги и номер километра.
data.loc[data.address.str.contains('МКАД'), 'street'] = \
data[data.address.str.contains('МКАД')].address.str.split(',').apply(lambda x: ','.join(x[1:3]))
data.head()
| index | name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | street | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | wowфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | 0 | NaN | улица Дыбенко |
| 1 | 1 | четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | 0 | 4.0 | улица Дыбенко |
| 2 | 2 | хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | 0 | 45.0 | Клязьминская улица |
| 3 | 3 | dormouse coffee shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | 0 | NaN | улица Маршала Федоренко |
| 4 | 4 | иль марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | 1 | 148.0 | Правобережная улица |
Добавим отдельный столбец, где будет указано работает ли заведение 24/7.
#Создаем новый столбец
data['is_24/7'] = np.nan
#Добавляем True, для 24/7 заведений
data.loc[(data.hours.str.contains('ежедневно, круглосуточно')) & (data.hours.notna()), 'is_24/7'] = \
data.loc[(data.hours.str.contains('ежедневно, круглосуточно')) & (data.hours.notna()), 'is_24/7'].fillna(True)
#Для остальный заведений добавляем False
data['is_24/7'].fillna(False, inplace=True)
#Посмотрим, сколько всего магазинов работает круглосуточно
data['is_24/7'].value_counts()
False 7666 True 739 Name: is_24/7, dtype: int64
chain == 1 не повторяются с названиями несетевых заведений.Была создана и использована функция для восстановления пропусков в сетевых заведениях, кол-во пропусков уменьшилось на ~2,5 тысячи.
#создадим агрегируемую таблицу
data_group = data.groupby('category', as_index=False).agg({'rating': 'count'})
data_group.columns=['category', 'amount']
data_group['procent'] = data_group.amount / data.shape[0]
#создадим и настроим столбчатую диграму
fig = px.bar(data_group.sort_values('amount'), y='category', x='amount', hover_data={'procent':':.1%'},
text='amount', color_discrete_sequence=px.colors.qualitative.Plotly)
fig.update_layout(title='Количество объектов общественного питания по категориям',
xaxis_title='Количество объектов', yaxis_title='Категория', template="simple_white")
fig.update_xaxes(range=[100, 2450], tickvals=np.arange(0, 2600, 250), showgrid=True)
fig.show()
Промежуточный вывод: В датасете представленные данные о заведениях из 8 категорий:
#создаем агрегируемый датафрейм и добавляем общую категорию
data_group = data.groupby('category', as_index=False).agg({'seats': ['median', 'mean']})
data_group.columns=['category', 'median', 'mean']
data_group.loc[len(data_group.index)] = ['все заведения', round(data.seats.median()), round(data.seats.mean())]
data_group
#строим и настраиваем столбчатую диаграмму
fig = px.bar(data_group.sort_values('median'), x='median', y='category', text='median',
hover_data=['mean'], color_discrete_sequence=px.colors.qualitative.Plotly)
fig.update_layout(title='Медианное количество посадочных мест в заведениях по категориям',
xaxis_title='Медианное количество посадочных мест',
yaxis_title='Категория заведения', template="simple_white")
fig.update_xaxes(range=[40, 91], showgrid=True)
fig.show()
Промежуточный вывод:
#Создаем переменные для постороения диаграмы
names = data.chain.value_counts().index
values = data.chain.value_counts()
#Строим круговую диаграмму
fig = px.pie(names=['Несетевые заведения', 'Сетевые заведения'], values=values, color_discrete_sequence=px.colors.qualitative.Pastel)
fig.update_traces(textposition='inside', textinfo='percent+label+value')
fig.update_layout(title='Соотношение сетевых и несетевых заведений',
width=750, height=600)
fig.show()
#Создадим агрегированную таблицу
data_chain_group = data.groupby(['category', 'chain']).name.count().reset_index()
data_chain_group = data_chain_group.merge(data.groupby('category').name.count().reset_index(), on='category')
data_chain_group.columns = ['category', 'chain', 'count', 'total']
data_chain_group.loc[len(data_chain_group.index)] = ['Все заведения', 0,
data_chain_group.query('chain == 0')['count'].sum(), data.shape[0]]
data_chain_group.loc[len(data_chain_group.index)] = ['Все заведения', 1,
data_chain_group.query('chain == 1')['count'].sum(), data.shape[0]]
data_chain_group['procent'] = data_chain_group['count'] / data_chain_group.total
data_chain_group['procent_2'] = (data_chain_group['procent'] * 100).round(1)
data_chain_group = data_chain_group.replace([0, 1], ['Несетевые заведения', 'Сетевые заведения']).sort_values('procent')
#Строим и настраиваем график
fig = px.bar(data_chain_group, y='category', x='procent', color='chain',
color_discrete_sequence=px.colors.qualitative.Pastel, text='procent_2',
hover_data={'procent':':.1%', 'count': ':', 'procent_2':'', 'total':':'})
fig.update_traces(textfont_size=12, textangle=0)
fig.update_xaxes(ticktext=list(map(lambda x: str(x) + '%', np.arange(0, 110, 10))),
tickvals=np.arange(0, 1.1, 0.1), showgrid=True)
fig.update_layout(height=500, title='Соотношение сетевых и несетевых заведений по категориям',
xaxis_title='Соотношение количества заведений',
yaxis_title='Категория', template='simple_white')
fig.show()
Промежуточный вывод:
В датасете присутсвует 5201 несетевое заведение и 3204 сетевых заведений. Сетевых заведений довольно много, это может быть связанно с тем, что для сетевых заведений рынок Москвы хорошо изучен и понятен, поэтому им не так сложно масштабироваться. Кроме того, Москва огромный мегаполис, а место работы, жизни и отдыха человека, может находиться в разных концах города, и человеку часто проще зайти в знакомое сетевое заведение, чем искать новое.
#Сгруппируем данные по названию и выведем кол-во заведений в каждой сети, и то к какой категории они относятся
top_rest = data.query('chain == 1').groupby('name', as_index=False) \
.agg({'district': 'count', 'category': 'unique'}) \
.sort_values('district', ascending=False).head(15)
top_rest.columns = ['name', 'count', 'category']
display(top_rest)
#Создадим и выведим распределение категорий среди топ-15 сетей
top_rest_category = data.query('name.isin(@top_rest.name) & chain == 1').category.value_counts().reset_index()
top_rest_category
| name | count | category | |
|---|---|---|---|
| 729 | шоколадница | 120 | [кофейня, кафе] |
| 335 | домино'с пицца | 76 | [пиццерия] |
| 331 | додо пицца | 74 | [пиццерия] |
| 146 | one price coffee | 71 | [кофейня] |
| 742 | яндекс лавка | 69 | [ресторан] |
| 58 | cofix | 65 | [кофейня] |
| 168 | prime | 50 | [ресторан, кафе] |
| 664 | хинкальная | 44 | [быстрое питание, кафе, ресторан, столовая, ба... |
| 409 | кофепорт | 42 | [кофейня] |
| 418 | кулинарная лавка братьев караваевых | 39 | [кафе] |
| 628 | теремок | 38 | [ресторан, быстрое питание] |
| 683 | чайхана | 37 | [кафе, быстрое питание, ресторан] |
| 39 | cofefest | 32 | [кофейня, кафе] |
| 267 | буханка | 32 | [булочная, кофейня, кафе] |
| 477 | му-му | 27 | [кафе, ресторан, кофейня, быстрое питание, пиц... |
| index | category | |
|---|---|---|
| 0 | кофейня | 336 |
| 1 | ресторан | 186 |
| 2 | пиццерия | 151 |
| 3 | кафе | 100 |
| 4 | булочная | 25 |
| 5 | быстрое питание | 12 |
| 6 | бар,паб | 4 |
| 7 | столовая | 2 |
# строим и оформляем столбчатую диаграмму по сетям
fig = px.bar(top_rest.sort_values('count'), x='count', y='name', text='count',
color_discrete_sequence=px.colors.qualitative.Plotly)
fig.update_layout(height=500, title='Топ-15 сетей по количеству заведений',
xaxis_title='Количество заведений в сети',
yaxis_title='Название сети', template="simple_white")
fig.update_xaxes(showgrid=True)
fig.show()
# строим и оформляем столбчатую диаграмму по категориям в сетях
fig = px.bar(top_rest_category.sort_values('category'), x='category', y='index',
text='category', color_discrete_sequence=px.colors.qualitative.Plotly)
fig.update_layout(title='Количество заведений по категориям в топ-15 сетях',
xaxis_title='Количество заведений в категории',
yaxis_title='Категория заведений', template="simple_white")
fig.update_xaxes(showgrid=True)
fig.show()
#Также посмотрим, как распределенны категории заведений среди сетей
top_rest_category_div = \
pd.pivot_table(data.query('name.isin(@top_rest.name)'), index='name', columns='category',
values='district', aggfunc={'district': 'count'}, fill_value=0)
top_rest_category_div['sum'] = top_rest_category_div.sum(axis=1)
top_rest_category_div = top_rest_category_div.replace({0: '-'})
top_rest_category_div.sort_values('sum', ascending=False)
| category | бар,паб | булочная | быстрое питание | кафе | кофейня | пиццерия | ресторан | столовая | sum |
|---|---|---|---|---|---|---|---|---|---|
| name | |||||||||
| шоколадница | - | - | - | 1 | 119 | - | - | - | 120 |
| домино'с пицца | - | - | - | - | - | 77 | - | - | 77 |
| додо пицца | - | - | - | - | - | 74 | - | - | 74 |
| one price coffee | - | - | - | - | 72 | - | - | - | 72 |
| яндекс лавка | - | - | - | - | - | - | 69 | - | 69 |
| cofix | - | - | - | - | 65 | - | - | - | 65 |
| prime | - | - | - | 1 | - | - | 49 | - | 50 |
| хинкальная | 3 | - | 6 | 19 | - | - | 15 | 1 | 44 |
| кофепорт | - | - | - | - | 42 | - | - | - | 42 |
| кулинарная лавка братьев караваевых | - | - | - | 39 | - | - | - | - | 39 |
| теремок | - | - | 2 | - | - | - | 36 | - | 38 |
| чайхана | - | - | 2 | 26 | - | - | 9 | - | 37 |
| cofefest | - | - | - | 1 | 31 | - | - | - | 32 |
| буханка | - | 25 | - | 1 | 6 | - | - | - | 32 |
| му-му | 1 | - | 2 | 12 | 2 | 1 | 8 | 1 | 27 |
#Также посмотрим, как распределенны цены по заведениям среди сетей
top_rest_price_div = \
pd.pivot_table(data.query('name.isin(@top_rest.name)'), index='name', columns='price',
values='district', aggfunc={'district': 'count'}, fill_value=0)
top_rest_price_div['sum'] = top_rest_price_div.sum(axis=1)
top_rest_price_div = top_rest_price_div.replace({0: '-'})
top_rest_price_div.sort_values('sum', ascending=False)
| price | высокие | выше среднего | низкие | средние | sum |
|---|---|---|---|---|---|
| name | |||||
| шоколадница | - | 2 | 1 | 104 | 107 |
| додо пицца | - | - | - | 74 | 74 |
| домино'с пицца | - | - | 1 | 72 | 73 |
| one price coffee | - | - | - | 72 | 72 |
| cofix | - | - | 30 | 18 | 48 |
| кофепорт | - | - | 42 | - | 42 |
| хинкальная | 5 | 3 | - | 25 | 33 |
| cofefest | - | - | - | 32 | 32 |
| кулинарная лавка братьев караваевых | - | - | 2 | 27 | 29 |
| prime | - | - | 10 | 17 | 27 |
| му-му | - | - | - | 27 | 27 |
| чайхана | - | 1 | 1 | 25 | 27 |
| теремок | - | - | 4 | 22 | 26 |
| буханка | - | - | 1 | 9 | 10 |
Промежуточный вывод
#Создадим агрегируемую таблицу
district_group = data.groupby(['district', 'category'], as_index=False).name.count()
#добавим столбец с общим кол-вом заведений в районе
district_group = district_group.merge(district_group.groupby('district').name.sum() \
.reset_index().rename(columns={'name': 'sum'}), how='left')
#Выведем таблицу с общим кол-вом заведений по районам
display(data.district.value_counts().reset_index())
# строим и настраиваем столбчатую диаграмму
fig = px.bar(district_group.sort_values(['sum']), y='district', x='name', color='category',
color_discrete_sequence=px.colors.qualitative.Pastel)
fig.update_layout(title='Количество заведений по административным районом Москвы',
xaxis_title='Количество заведений по категориям в районе',
yaxis_title='Административный район', template="simple_white")
fig.update_xaxes(tickvals=np.arange(0, 2300, 250), showgrid=True)
fig.show()
| index | district | |
|---|---|---|
| 0 | Центральный административный округ | 2242 |
| 1 | Северный административный округ | 899 |
| 2 | Южный административный округ | 892 |
| 3 | Северо-Восточный административный округ | 891 |
| 4 | Западный административный округ | 851 |
| 5 | Восточный административный округ | 798 |
| 6 | Юго-Восточный административный округ | 714 |
| 7 | Юго-Западный административный округ | 709 |
| 8 | Северо-Западный административный округ | 409 |
Промежуточный вывод
rating_data = data.groupby('category').rating.mean().round(2).reset_index()
rating_data.loc[len(rating_data.index)] = ['все заведения', data.rating.mean().round(2)]
# строим и настраиваем столбчатую диаграмму
fig = px.bar(rating_data.sort_values('rating'),
y='category', x='rating', text='rating', color_discrete_sequence=px.colors.qualitative.Plotly)
# оформляем график
fig.update_layout(title='График среднего рейтинга по категориям заведений',
xaxis_title='Средний рейтинг',
yaxis_title='Категория заведения', template='simple_white')
fig.update_xaxes(range=[4, 4.4], showgrid=True)
fig.show()
Промежуточный вывод:
# загружаем JSON-файл с границами округов Москвы
state_geo = 'https://code.s3.yandex.net/data-analyst/admin_level_geomap.geojson'
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')
district_rating_group = data.groupby('district').rating.mean().round(2).reset_index()
#Создаем и настраиваем хроноплет
choropleth = Choropleth(
geo_data=state_geo,
data=district_rating_group,
columns=['district', 'rating'],
key_on='feature.name',
fill_color='YlGnBu',
fill_opacity=0.8,
line_opacity=0.3,
name='Средний рейтинг по округам',
highlight=True,
overlay=True,
show=True,
legend_name='Средний рейтинг заведений',
bins=np.arange(4.1, 4.4, 0.05)
).add_to(m)
#Добавляем json хроноплета информацию о медианном среднем чеке
for i in choropleth.geojson.data['features']:
i['properties']['mean_rating'] = \
float(district_rating_group.loc[district_rating_group.district == i['name'], 'rating'])
#Добавляем отображение названия округа и медианного среднего чек
choropleth.geojson.add_child(
folium.features.GeoJsonTooltip(['name', 'mean_rating'],labels=True, aliases=['Округ', 'Средний рейтинг'])
)
#Создаем пустой список, куда будем добавлять кластеры заведений по районам
marker_cluster = [None] * len(data.district.unique())
#создаем функцию для добавления точек в кластер
def create_clusters(row):
Marker([row['lat'], row['lng']],
popup=f"Название: {row['name']} \nРейтинг: {row['rating']} \nКатегория: {row['category']}"
).add_to(marker_cluster[index])
#Проходимся по всем районам
for index, district in enumerate(data.district.unique()):
#создаём пустой кластер для района и добавляем его на карту
marker_cluster[index] = MarkerCluster(name=district).add_to(m)
#берем заведения района и добавляем их точки в кластер
data.loc[data.district == district].apply(create_clusters, axis=1)
#добавляем управление слоями
folium.LayerControl().add_to(m)
# выводим карту
m
#Таже отдельно выведим средний рейтинг по округам
district_rating = data.groupby('district').rating.mean().round(2).reset_index()
district_rating.loc[len(district_rating.index)] = ['Все заведения', data.rating.mean().round(2)]
display(district_rating.sort_values('rating', ascending=False))
#Также выведем таблицу со средний рейтингом каждой категории по округам
pd.pivot_table(data, columns='district', index='category', values='rating', aggfunc='mean').round(2)
| district | rating | |
|---|---|---|
| 5 | Центральный административный округ | 4.38 |
| 2 | Северный административный округ | 4.24 |
| 9 | Все заведения | 4.23 |
| 4 | Северо-Западный административный округ | 4.21 |
| 1 | Западный административный округ | 4.18 |
| 8 | Южный административный округ | 4.18 |
| 0 | Восточный административный округ | 4.17 |
| 7 | Юго-Западный административный округ | 4.17 |
| 3 | Северо-Восточный административный округ | 4.15 |
| 6 | Юго-Восточный административный округ | 4.10 |
| district | Восточный административный округ | Западный административный округ | Северный административный округ | Северо-Восточный административный округ | Северо-Западный административный округ | Центральный административный округ | Юго-Восточный административный округ | Юго-Западный административный округ | Южный административный округ |
|---|---|---|---|---|---|---|---|---|---|
| category | |||||||||
| бар,паб | 4.32 | 4.40 | 4.33 | 4.19 | 4.39 | 4.49 | 4.20 | 4.35 | 4.28 |
| булочная | 4.17 | 4.26 | 4.25 | 4.34 | 4.28 | 4.37 | 4.04 | 4.16 | 4.34 |
| быстрое питание | 4.04 | 3.97 | 3.98 | 4.03 | 3.95 | 4.23 | 3.93 | 4.09 | 4.10 |
| кафе | 4.10 | 4.08 | 4.18 | 4.05 | 4.05 | 4.30 | 4.05 | 4.04 | 4.09 |
| кофейня | 4.28 | 4.20 | 4.29 | 4.22 | 4.33 | 4.34 | 4.23 | 4.28 | 4.23 |
| пиццерия | 4.27 | 4.29 | 4.29 | 4.26 | 4.34 | 4.41 | 4.19 | 4.34 | 4.26 |
| ресторан | 4.19 | 4.26 | 4.29 | 4.21 | 4.29 | 4.42 | 4.16 | 4.23 | 4.21 |
| столовая | 4.23 | 4.11 | 4.22 | 4.08 | 4.19 | 4.32 | 4.10 | 4.24 | 4.26 |
Промежуточный вывод
#Сгрупируем таблицу по улицам и выведем топ-15 улиц
top_street = data.groupby('street').agg({'chain': 'count', 'rating': 'mean', 'middle_avg_bill': 'mean'}) \
.sort_values('chain', ascending=False).head(15).reset_index().round(2)
top_street.columns = ['street', 'total', 'rating', 'middle_avg_bill']
top_street
| street | total | rating | middle_avg_bill | |
|---|---|---|---|---|
| 0 | проспект Мира | 184 | 4.12 | 813.72 |
| 1 | Профсоюзная улица | 122 | 4.19 | 723.12 |
| 2 | проспект Вернадского | 108 | 4.22 | 953.84 |
| 3 | Ленинский проспект | 107 | 4.21 | 1043.95 |
| 4 | Ленинградский проспект | 95 | 4.29 | 1131.50 |
| 5 | Дмитровское шоссе | 88 | 4.15 | 752.22 |
| 6 | Каширское шоссе | 77 | 4.14 | 698.91 |
| 7 | Варшавское шоссе | 76 | 4.20 | 678.43 |
| 8 | Ленинградское шоссе | 70 | 4.17 | 1071.27 |
| 9 | Люблинская улица | 60 | 4.13 | 568.86 |
| 10 | улица Вавилова | 55 | 4.26 | 679.55 |
| 11 | Кутузовский проспект | 54 | 4.33 | 1700.96 |
| 12 | улица Миклухо-Маклая | 49 | 4.18 | 691.58 |
| 13 | Пятницкая улица | 48 | 4.46 | 1138.26 |
| 14 | Алтуфьевское шоссе | 47 | 4.12 | 692.12 |
#Создаем сгрупированную таблицу по топ-15 улицам и категориям
top_street_group = data.query('street.isin(@top_street.street)').groupby(['street', 'category'], as_index=False).name.count()
#Добавляем процент и общее кол-во заведений на улице
top_street_group = top_street_group.merge(top_street[['street', 'total']], how='left', on='street')
top_street_group['procent'] = top_street_group['name'] / top_street_group['total']
top_street_group['procent_2'] = (top_street_group['procent'] * 100).round()
top_street_group = top_street_group.rename(columns={'name': 'total_on_street'}) \
.sort_values(['total', 'total_on_street'], ascending=(True, False))
#Строим и настраиваем график
fig = px.bar(top_street_group, y='street', x='procent', color='category',
color_discrete_sequence=px.colors.qualitative.Pastel, text='procent_2',
hover_data={'procent':':.1%', 'total_on_street': ':', 'total':':', 'street': '', 'procent_2': ''})
fig.update_traces(textfont_size=12, textangle=0)
fig.update_xaxes(ticktext=list(map(lambda x: str(x) + '%', np.arange(0, 110, 10))),
tickvals=np.arange(0, 1.1, 0.1), showgrid=True)
fig.update_layout(height=500, title='Соотношение количества заведений по категориям на топ-15 улицах',
xaxis_title='Соотношение количества заведений по категориям',
yaxis_title='Улица', template='simple_white')
fig.show()
Также посмотрим на карте на улицы и заведения на них
#Выберим только нужные столбцы и строки с заведениями из топ-15 улиц
top_street_line = data.query('street.isin(@top_street.street)')[['name', 'lat', 'lng', 'rating', 'street']] \
.sort_values(['street', 'lat'])
#добавим общее кол-во заведений на улице
top_street_line = top_street_line.merge(top_street[['street', 'total']], how='left', on='street')
moscow_lat, moscow_lng = 55.751244, 37.618423
# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')
#создаем функцию для создание линнии улицы
def create_line(coordinates, index, street):
PolyLine(
smooth_factor=20,
locations=coordinates,
color=px.colors.qualitative.Dark24[index],
weight=2,
tooltip=f"{street} - {top_street_line.query('street == @street')['total'].min()} заведений",
).add_to(m)
#Проходимся по улицам и добавляем их на карту
for index, street in enumerate(top_street_line.street.unique()):
create_line(top_street_line.query('street == @street')[['lat', 'lng']], index, street)
#Создаем пустой список, куда будем добавлять кластеры заведений по улицас
marker_cluster = [None] * len(top_street.street)
#создаем функцию для добавления точек в кластер
def create_clusters(row):
Marker([row['lat'], row['lng']],
popup=f"Название: {row['name']} \nРейтинг: {row['rating']} \nКатегория: {row['category']}"
).add_to(marker_cluster[index])
#Проходимся по всем улицам
for index, street in enumerate(top_street.street):
#создаём пустой кластер для улицы и добавляем его на карту
marker_cluster[index] = MarkerCluster(name=street, show=False).add_to(m)
#берем заведения улицы и добавляем их точки в кластер
data.loc[data.street == street].apply(create_clusters, axis=1)
#добавляем управление слоями
folium.LayerControl().add_to(m)
m
Промежуточный вывод
#Создадим два датафрема с заведениями, только с одним заведением на улице и с несколькими
one_rest_street = data.street.value_counts()
one_rest_street = one_rest_street[one_rest_street == 1]
one_rest = data.query('street.isin(@one_rest_street.index)')
not_one_rest = data.query('not street.isin(@one_rest_street.index)')
print('{:-^60}'.format(' Описательная статистика улиц с одним заведением '))
display(one_rest.iloc[:, 7:].describe().round(2))
print('{:-^60}'.format(' Описательная статистика улиц с несколькими заведениями '))
display(not_one_rest.iloc[:, 7:].describe().round(2))
print('{:-^60}'.format(' Разница значений описательной статистики '))
display((one_rest.iloc[:, 7:].describe() - not_one_rest.iloc[:, 7:].describe()).round(2))
print('-'*60)
print('\nКоличество сетевых заведений на улицах с одним заведением:',
one_rest.query('chain==1').name.count())
print('Соотношение сетевых заведений к несетевым на улицах с одним заведением:',
f'''{(one_rest.query('chain==1').name.count() / one_rest.shape[0]):.1%}''')
print('\nКоличество сетевых ресторанов на улицах с несколькими заведениями:',
not_one_rest.query('chain==1').name.count())
print('Соотношение сетевых заведений к несетевым на улицах с несколькими заведениями:',
f'''{(not_one_rest.query('chain==1').name.count() / not_one_rest.shape[0]):.1%}''')
----- Описательная статистика улиц с одним заведением ------
| lng | rating | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|
| count | 469.00 | 469.00 | 191.00 | 32.00 | 469.00 | 189.00 |
| mean | 37.62 | 4.23 | 894.96 | 177.97 | 0.30 | 59.52 |
| std | 0.10 | 0.46 | 951.95 | 51.69 | 0.46 | 49.12 |
| min | 37.39 | 1.00 | 67.00 | 95.00 | 0.00 | 0.00 |
| 25% | 37.56 | 4.10 | 300.00 | 140.00 | 0.00 | 30.00 |
| 50% | 37.61 | 4.30 | 600.00 | 170.00 | 0.00 | 45.00 |
| 75% | 37.67 | 4.50 | 1250.00 | 197.50 | 1.00 | 80.00 |
| max | 37.87 | 5.00 | 7000.00 | 320.00 | 1.00 | 273.00 |
-- Описательная статистика улиц с несколькими заведениями --
| lng | rating | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|
| count | 7936.00 | 7936.00 | 3441.00 | 651.00 | 7936.00 | 5051.00 |
| mean | 37.61 | 4.23 | 899.14 | 163.23 | 0.39 | 109.88 |
| std | 0.10 | 0.47 | 767.29 | 66.53 | 0.49 | 123.45 |
| min | 37.36 | 1.00 | 30.00 | 60.00 | 0.00 | 0.00 |
| 25% | 37.54 | 4.10 | 362.00 | 101.00 | 0.00 | 40.00 |
| 50% | 37.60 | 4.30 | 650.00 | 160.00 | 0.00 | 75.00 |
| 75% | 37.66 | 4.40 | 1250.00 | 214.00 | 1.00 | 140.00 |
| max | 37.87 | 5.00 | 10000.00 | 375.00 | 1.00 | 1288.00 |
--------- Разница значений описательной статистики ---------
| lng | rating | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|
| count | -7467.00 | -7467.00 | -3250.00 | -619.00 | -7467.00 | -4862.00 |
| mean | 0.01 | 0.00 | -4.18 | 14.74 | -0.09 | -50.36 |
| std | -0.00 | -0.01 | 184.67 | -14.84 | -0.03 | -74.33 |
| min | 0.03 | 0.00 | 37.00 | 35.00 | 0.00 | 0.00 |
| 25% | 0.02 | 0.00 | -62.00 | 39.00 | 0.00 | -10.00 |
| 50% | 0.01 | 0.00 | -50.00 | 10.00 | 0.00 | -30.00 |
| 75% | 0.00 | 0.10 | 0.00 | -16.50 | 0.00 | -60.00 |
| max | -0.01 | 0.00 | -3000.00 | -55.00 | 0.00 | -1015.00 |
------------------------------------------------------------ Количество сетевых заведений на улицах с одним заведением: 140 Соотношение сетевых заведений к несетевым на улицах с одним заведением: 29.9% Количество сетевых ресторанов на улицах с несколькими заведениями: 3064 Соотношение сетевых заведений к несетевым на улицах с несколькими заведениями: 38.6%
#Создадим и настроим диаграммы, чтобы сравнить соотношения категорий заведений на этих улицах
#Создадим агрегированные таблицы для двух тип улиц
one_rest_grouped = one_rest.groupby('category').name.count().reset_index()
one_rest_grouped['total'] = one_rest.shape[0]
one_rest_grouped['procent'] = (one_rest_grouped.name / one_rest_grouped.total * 100).round(1)
one_rest_grouped['street'] = 'одно заведение'
not_one_rest_grouped = not_one_rest.groupby('category').name.count().reset_index()
not_one_rest_grouped['total'] = not_one_rest.shape[0]
not_one_rest_grouped['procent'] = (not_one_rest_grouped.name / not_one_rest_grouped.total * 100).round(1)
not_one_rest_grouped['street'] = 'несколько заведений'
#Строим и настраиваем график
fig = go.Figure(data=[
go.Bar(name='Улицы с одним заведением', y=one_rest_grouped.sort_values('procent').category, x=one_rest_grouped.procent,
orientation='h', text=one_rest_grouped.procent,
marker_color=px.colors.qualitative.Pastel[0], base=0),
go.Bar(name='Улицы с несколькими заведениями', y=not_one_rest_grouped.category, x=not_one_rest_grouped.procent,
orientation='h', text=not_one_rest_grouped.procent,
marker_color=px.colors.qualitative.Pastel[1], base=0)
])
fig.update_layout(barmode='group', template='simple_white', height=600,
title='Соотношения категорий заведений по улицам',
xaxis_title='Доля заведений',
yaxis_title='Категория',
legend={'title':{'text':'Тип улиц'}, 'font_size':12})
fig.update_xaxes(tickvals=np.arange(0, 45, 5),
ticktext=list(map(lambda x: str(x) + '%', np.arange(0, 45, 5))),showgrid=True)
fig.show()
#Посмотрим на карту, где находятся эти заведения
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')
# создаём пустой кластер, добавляем его на карту
marker_cluster = MarkerCluster().add_to(m)
def create_clusters(row):
# сохраняем URL-адрес изображения со значком торгового центра с icons8,
# это путь к файлу на сервере icons8
icon_url = 'https://img.icons8.com/metro/26/food.png'
# создаём объект с собственной иконкой размером 30x30
icon = CustomIcon(icon_url, icon_size=(30, 30))
# создаём маркер с иконкой icon и добавляем его в кластер
Marker(
[row['lat'], row['lng']],
popup=f"Название: {row['name']} \nРейтинг: {row['rating']} \
\nКатегория: {row['category']} \n{row['street']}"
).add_to(marker_cluster)
# применяем функцию для создания кластеров к каждой строке датафрейма
one_rest.apply(create_clusters, axis=1)
# выводим карту
m
Промеуточный вывод
#Создадим карту с хроноплетом медианого среднего чека
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')
#Создаем агрегированную таблицу
district_median_group = data.groupby('district').middle_avg_bill.median().round().reset_index()
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
choropleth_bill = Choropleth(
geo_data=state_geo,
data=district_median_group,
columns=['district', 'middle_avg_bill'],
key_on='feature.name',
fill_color='YlGnBu',
fill_opacity=0.8,
line_opacity=0.3,
highlight=True,
legend_name='Медианный средний чек',
name='Медианный средний чек',
bins=np.arange(400, 1200, 100)
).add_to(m)
#Добавляем json хроноплета информацию о медианном среднем чеке
for i in choropleth_bill.geojson.data['features']:
i['properties']['middle_avg_bill'] = \
int(district_median_group.loc[district_median_group.district == i['name'], 'middle_avg_bill'])
#Добавляем отображение названия округа и медианного среднего чек
choropleth_bill.geojson.add_child(
folium.features.GeoJsonTooltip(['name', 'middle_avg_bill'],labels=True, aliases=['Округ', 'Медиана'])
)
#добавляем управление слоями
folium.LayerControl().add_to(m)
# выводим карту
m
Для лучшего понимания зависимости среднего чека от центра, сделать хитмеп на карте. Для создания хитмепа, нужно создать новый Geojson с нужными параметрами, для это создадим функцию.
#Создадим функция для создания geojson для хитмепа. В аргументах будет указан размер клеток хитмепа,
#Столбец и функция для агрегирования, ограничения для огрегированных значений
def map_heatmap(data, section_lat=40, section_lng=40, values='middle_avg_bill',
aggfunc='median', name_value='median_avg_bill', limit_min = None, limit_max = None):
#Создадим таблицу, где координаты будут поделены на заданное кол-во интервалов,
#и для каждого пересечение интервалов посчитаем среднее или медиану заданого значения.
#В итоге каждое пересечение интервалов координат, создаст квадратик хитмепа со средним или медианным значением
new_data = pd.concat([data[[values] + ['district']],
pd.cut(data.lat, bins=section_lat, precision=6),
pd.cut(data.lng, bins=section_lng, precision=6)], axis=1).groupby(['lat', 'lng']) \
.agg({values: aggfunc, 'district': 'unique'}) \
.reset_index()
#Удалим пересечения, где нет информации о значение или районе
new_data = new_data.loc[(new_data[values].notna()) & (new_data['district'].notna())].reset_index(drop=True)
#Создадим функцию, которая будет создавать последовательность координат для создания квадратика хитмепа
def coordinates(row):
return [[[row.lng.left, row.lat.left], [row.lng.right, row.lat.left],
[row.lng.right, row.lat.right], [row.lng.left, row.lat.right]]]
#Воспользуемся функцией
new_data['coordinates'] = new_data.apply(coordinates, axis=1)
#Создадим функцию, которая будет создавать полигон GeoJSON с заданными параметрами и полигонами
def feature_create(row):
return Feature(geometry=Polygon(row.coordinates),
properties={name_value: row[values], 'name':row.name, 'district':row.district[0]},
id=row.name)
#Воспользуемся функцией
new_data['feature'] = new_data.apply(feature_create, axis=1)
#Объеденим все полигоны с задаными параметрами в один файл для создания карты
FeatureCollection_heatmap = FeatureCollection([i for i in new_data['feature']])
#Соеденять нашу таблицу с картой будем по индексам, поэтому создадим столбец с индексами
new_data = new_data.reset_index()
#Для квадратиков, где среднее/медианное значение будет выходить за установленный лимит(если он установлен),
#чтобы в дальнейшим их выделить своим цветов
if limit_min != None:
new_data.loc[new_data[values] < limit_min, values] = None
if limit_max != None:
new_data.loc[new_data[values] >= limit_max, values] = None
return new_data, FeatureCollection_heatmap
#Создадим карту с тепловой картой медианого среднего чека и границами округов
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')
#Создаем хроноплет, где будут показаны границы округов
choropleth = folium.Choropleth(
geo_data=state_geo,
fill_opacity=0,
line_weight=2,
line_color='grey',
highlight=True,
name='Границы округов и их медианный рейтинг'
).add_to(m)
#Добавим в хроноплет информацию о медианном среднем чеке округа
for i in choropleth.geojson.data['features']:
i['properties']['middle_avg_bill'] = \
int(district_median_group.loc[district_median_group.district == i['name'], 'middle_avg_bill'])
#Добавляем отображение названия округа и медианного среднего чека
choropleth.geojson.add_child(
folium.features.GeoJsonTooltip(['name', 'middle_avg_bill'],labels=True, aliases=['Округ', 'Медиана'])
)
#Создадим данные для хитмепа карты
median_map_data, FeatureCollection_heatmap = map_heatmap(data, section_lat=40, section_lng=40, values='middle_avg_bill',
aggfunc='median', name_value='median_avg_bill', limit_min = None, limit_max = 2000)
# создаём хроноплет для отображения хитмепа и настраиваем его
choropleth_sq = Choropleth(
geo_data=FeatureCollection_heatmap,
data=median_map_data,
columns=['index', 'middle_avg_bill'],
key_on='feature.properties.name',
fill_color='YlGnBu',
fill_opacity=0.8,
line_opacity=0.3,
line_weight=1,
nan_fill_color='purple',
nan_fill_opacity=0.8,
highlight=True,
legend_name='Медианный средний чек тепловой карты',
name='Медианный средний чек/тепловая карта',
bins=range(0, 2100, 250),
).add_to(m)
#Добавляем отображение названия округа и медианного среднего чек при наведение на кубик хитмепа
choropleth_sq.geojson.add_child(
folium.features.GeoJsonTooltip(['district', 'median_avg_bill'], labels=True, aliases=['Полигон в', 'Медиана'])
)
#добавляем управление слоями
folium.LayerControl().add_to(m)
# выводим карту
m
#Выведим описательную статистику среднего чека для каждого округа и общую для всех заведений
district_describe = data.groupby('district').middle_avg_bill.describe()
district_describe.loc['Все округа'] = list(data.middle_avg_bill.describe().reset_index().T.iloc[1])
district_describe.sort_values('50%', ascending=False)
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| district | ||||||||
| Центральный административный округ | 1207.0 | 1125.961889 | 903.768784 | 67.0 | 436.50 | 1000.0 | 1500.0 | 7250.0 |
| Западный административный округ | 344.0 | 1027.898256 | 772.192942 | 50.0 | 400.00 | 850.0 | 1500.0 | 5250.0 |
| Северо-Западный административный округ | 172.0 | 793.162791 | 591.655530 | 100.0 | 311.25 | 662.5 | 1100.0 | 2900.0 |
| Все округа | 3632.0 | 898.916024 | 777.933495 | 30.0 | 350.00 | 650.0 | 1250.0 | 10000.0 |
| Северный административный округ | 381.0 | 834.068241 | 694.076608 | 100.0 | 325.00 | 600.0 | 1150.0 | 5000.0 |
| Юго-Западный административный округ | 281.0 | 728.156584 | 544.924238 | 100.0 | 344.00 | 525.0 | 1000.0 | 2750.0 |
| Восточный административный округ | 300.0 | 827.626667 | 945.582877 | 50.0 | 321.25 | 512.5 | 1000.0 | 10000.0 |
| Северо-Восточный административный округ | 355.0 | 684.126761 | 569.338806 | 50.0 | 302.00 | 500.0 | 862.5 | 4500.0 |
| Южный административный округ | 345.0 | 720.423188 | 548.410765 | 100.0 | 340.00 | 500.0 | 1000.0 | 3500.0 |
| Юго-Восточный административный округ | 247.0 | 622.327935 | 528.680273 | 30.0 | 275.00 | 450.0 | 750.0 | 3750.0 |
Промежуточный вывод
Медианая цена среднего чека самая высокая в ЦАО на всей его площади. В остальных же округах медианный средний более разнообразный, это видно на хитмепе карты, он может быть любым(низкой, средней, высокой и очень высокой).В связи с этим, можем сделать предположение, что для всех округов, кроме ЦАО, медианный средний чек больше зависит от других факторов, чем от расстояния до центра.
#Создадим отдельный датафрейм только с кофейнями
coffe_data = data.query('category == "кофейня"')
#Создадим агрегируемую таблицу
coffe_data_total = coffe_data.groupby('district').name.count().reset_index().rename(columns={'name': 'amount'})
#Добавим строку со средним кол-вом кофеен на округ
coffe_data_total.loc[len(coffe_data_total.index)] = ['Среднее кол-во кофеен на округ',
coffe_data.district.value_counts().mean()]
#Выведем кол-во и рейтинг кофеен по райнам и среди всех кофеен
print('-'*50)
display(coffe_data_total.sort_values('amount', ascending=False))
print('-'*50)
print('Количество кофеен в датасете:', coffe_data.shape[0])
print('-'*50)
#создадим и настроим столбчатую диграму
fig = px.bar(coffe_data_total.sort_values('amount'), y='district', x='amount',
text='amount', color_discrete_sequence=px.colors.qualitative.Plotly)
fig.update_layout(title='Количество кофеен в округах',
xaxis_title='Количество кофеен', yaxis_title='Округ', template="simple_white")
fig.update_xaxes(showgrid=True)
fig.show()
--------------------------------------------------
| district | amount | |
|---|---|---|
| 5 | Центральный административный округ | 428.0 |
| 2 | Северный административный округ | 193.0 |
| 3 | Северо-Восточный административный округ | 159.0 |
| 9 | Среднее кол-во кофеен на округ | 157.0 |
| 1 | Западный административный округ | 150.0 |
| 8 | Южный административный округ | 131.0 |
| 0 | Восточный административный округ | 105.0 |
| 7 | Юго-Западный административный округ | 96.0 |
| 6 | Юго-Восточный административный округ | 89.0 |
| 4 | Северо-Западный административный округ | 62.0 |
-------------------------------------------------- Количество кофеен в датасете: 1413 --------------------------------------------------
Промежуточный вывод
Создадим функцию, которая будет считать сколько кофеен находиться рядом с кофейней в заданом расстояние
#Создадим переменные, в которые длину широты и долготы, которые равны примерно 1 км.
lat_km = 0.00899
lng_km = 0.01604
#Создадим функция для подсчета ближащих кофеен рядом с исходной кофейни в заданном расстояние
def count_near_coffe(coordinates, distance):
#Посчитаем на сколько нам нужно поделить 1км долготы и широты, что получить заданое расстояние
div_km = 1000 / distance
#Для поиска ближайщих кофеен ограничим зону поиска со всех сторон заданным расстоянием и исключим исходную кофейню
data_near = coffe_data.loc[(coffe_data.index != coordinates.name) &
(coffe_data['lat'] <= coordinates['lat'] + lat_km / div_km) &
(coffe_data['lat'] >= coordinates['lat'] - lat_km / div_km) &
(coffe_data['lng'] <= coordinates['lng'] + lng_km / div_km) &
(coffe_data['lng'] >= coordinates['lng'] - lng_km / div_km)]
#Если в заданом пределе нет кафе, то вернем ноль
if data_near.shape[0] == 0:
return 0
else:
#Если в заданом пределе есть 1 или более кофеен,
#то используем функцию count_near_coffe_2 и сложим полученные результаты
return np.sum(data_near.apply(lambda row: count_near_coffe_2(row, coordinates, distance), axis=1))
#Функция считает расстояние между двум передаными точками и, если оно меньше или равно заданого расстояния возвращает 1
def count_near_coffe_2(row, coordinates, distance):
if round(geopy.distance.great_circle([row['lat'], row['lng']], [coordinates['lat'], coordinates['lng']]).m) <= distance:
return 1
else:
return 0
#Создадим новые столбцы, где для каждой кофейни найдем кол-во ближайщих кофеен на заданом расстояние(100м, 250м, 500м и 1км)
coffe_data['near_coffe_1000'] = coffe_data.apply(lambda coordinates: count_near_coffe(coordinates, 1000), axis=1)
coffe_data['near_coffe_500'] = coffe_data.apply(lambda coordinates: count_near_coffe(coordinates, 500), axis=1)
coffe_data['near_coffe_250'] = coffe_data.apply(lambda coordinates: count_near_coffe(coordinates, 250), axis=1)
coffe_data['near_coffe_100'] = coffe_data.apply(lambda coordinates: count_near_coffe(coordinates, 100), axis=1)
Создадим функцию, которая находит расстояние до ближайщей кофейни
#Создадим функцию
def nearest_coffe(coordinates):
#Зададим первоначальную зону поиска
max_distance = 100
#Создадим переменую с расстоянием до ближайщей кофейни
distance = 0
#Создадим цикл, которые будет искать ближайщую кофейню, постепенно увеличивая зону поиска
while True:
#Посчитаем на сколько надо разделить 1км долготы и широты, чтобы получить верную зону поиска
div_km = 1000 / max_distance
#Ограничиваем зону поиска
data_near = coffe_data.loc[(coffe_data.index != coordinates.name) &
(coffe_data['lat'] <= coordinates['lat'] + lat_km / div_km) &
(coffe_data['lat'] >= coordinates['lat'] - lat_km / div_km) &
(coffe_data['lng'] <= coordinates['lng'] + lng_km / div_km) &
(coffe_data['lng'] >= coordinates['lng'] - lng_km / div_km)]
#Если датасет с ограниченной зоной поиска пуст, то увеличиваем зону поиска и начинам сначала цикл
if data_near.shape[0] == 0:
max_distance = max_distance * 2
continue
else:
#Если датасет не пустой, то используем функцию nearest_coffe_2
#и получаем список расстояний до ближайщих кофеен и выбираем минимальное
distance = min(data_near.apply(lambda row: nearest_coffe_2(row, coordinates), axis=1))
break
#Возвращаем расстояние до ближайщей кофейни
return distance
#Создадим функцию, которая будет выдавать расстояние в метрах между двумя кофейнями
def nearest_coffe_2(row, coordinates):
return round(geopy.distance.great_circle([row['lat'], row['lng']],
[coordinates['lat'], coordinates['lng']]).m)
#Воспользуемся функцией и получим расстояние до ближайщей кофейни
coffe_data['nearest_coffe_m'] = coffe_data.apply(lambda coordinates: nearest_coffe(coordinates), axis=1)
#Выведим получившиесю таблицу
coffe_data.head()
| index | name | category | address | district | hours | lat | lng | rating | price | ... | middle_coffee_cup | chain | seats | street | is_24/7 | near_coffe_1000 | near_coffe_500 | near_coffe_250 | near_coffe_100 | nearest_coffe_m | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3 | 3 | dormouse coffee shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | ... | 170.0 | 0 | NaN | улица Маршала Федоренко | False | 2 | 1 | 0 | 0 | 295 |
| 25 | 25 | в парке вкуснее | кофейня | Москва, парк Левобережный | Северный административный округ | ежедневно, 10:00–21:00 | 55.878453 | 37.460028 | 4.3 | NaN | ... | NaN | 1 | NaN | парк Левобережный | False | 1 | 0 | 0 | 0 | 757 |
| 45 | 45 | 9 bar coffee | кофейня | Москва, Коровинское шоссе, 41, стр. 1 | Северный административный округ | пн-пт 08:00–18:00 | 55.885837 | 37.513422 | 4.0 | NaN | ... | 105.0 | 1 | 46.0 | Коровинское шоссе | False | 0 | 0 | 0 | 0 | 1259 |
| 46 | 46 | cofefest | кофейня | Москва, улица Маршала Федоренко, 6с1 | Северный административный округ | пн-пт 09:00–19:00 | 55.879934 | 37.492522 | 3.6 | средние | ... | NaN | 1 | 90.0 | улица Маршала Федоренко | False | 2 | 1 | 0 | 0 | 295 |
| 52 | 52 | cofix | кофейня | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 08:00–22:00 | 55.878531 | 37.479395 | 3.8 | средние | ... | NaN | 1 | NaN | улица Дыбенко | False | 3 | 0 | 0 | 0 | 682 |
5 rows × 22 columns
Создадим таблицу и график, где отобразим соотношение расстояний до ближайщих кофеин по округам. Также посмотрии на среднее и медианное расстояние до кофеен
#Создадим список индексов
index = ['от 0 до 100', 'от 100 до 250', 'от 250 до 500','от 500 до 1000','от 1000 до 2000']
#Создадим пустую таблицу
coffe_distance_div = pd.DataFrame()
#Создадим цикл, где будет добавлять в таблицу информацию по каждому району
for district in coffe_data.district.unique():
coffe_x = pd.DataFrame({'district':[district] * 5, 'distance': index})
coffe_x['total'] = pd.cut(coffe_data.query('district == @district').nearest_coffe_m,
bins=[0, 100, 250, 500, 1000, 2000]) \
.value_counts().reset_index(drop=True)
coffe_x['sum'] = coffe_x.total.sum()
coffe_x['procent'] = coffe_x.total / coffe_x.total.sum()
coffe_distance_div = pd.concat([coffe_distance_div, coffe_x]).reset_index(drop=True)
#Создадим отдельную таблицу для всех сразу кофеен и добавим ее в основную таблицу
coffe_all_district = pd.DataFrame({'district':'Все округа', 'distance':index,
'total':pd.cut(coffe_data.nearest_coffe_m,
bins=[0, 100, 250, 500, 1000, 2000]) \
.value_counts().reset_index(drop=True), 'sum':coffe_data.shape[0]})
coffe_all_district['procent'] = coffe_all_district['total'] / coffe_all_district['sum']
coffe_distance_div = pd.concat([coffe_distance_div, coffe_all_district])
#Выведим полученную таблицу
coffe_distance_div.head()
| district | distance | total | sum | procent | |
|---|---|---|---|---|---|
| 0 | Северный административный округ | от 0 до 100 | 83 | 193 | 0.430052 |
| 1 | Северный административный округ | от 100 до 250 | 47 | 193 | 0.243523 |
| 2 | Северный административный округ | от 250 до 500 | 42 | 193 | 0.217617 |
| 3 | Северный административный округ | от 500 до 1000 | 18 | 193 | 0.093264 |
| 4 | Северный административный округ | от 1000 до 2000 | 3 | 193 | 0.015544 |
#Строим и настраиваем график
fig = px.bar(coffe_distance_div.sort_values(['sum', 'total'], ascending=(True, False)),
y='district', x='procent', color='distance',
color_discrete_sequence=px.colors.qualitative.Pastel, text='total',
hover_data={'procent':':.1%', 'sum':':'})
fig.update_traces(textfont_size=12, textangle=0)
fig.update_xaxes(ticktext=list(map(lambda x: str(x) + '%', np.arange(0, 110, 10))),
tickvals=np.arange(0, 1.1, 0.1), showgrid=True)
fig.update_layout(height=500, title='Соотношение расстояния между ближайщими кофейними по округам',
xaxis_title='Соотношение количества кофеен по расстоянию',
yaxis_title='Округ', template='simple_white',
legend={'title':{'text':'Расстояние в метрах'}, 'font_size':12})
fig.show()
#Создадим таблицу со средним расстоянием по округам и выведим ее
coffe_nearest_district = coffe_data.groupby('district').nearest_coffe_m.agg(['mean', 'median']).round().reset_index()
coffe_nearest_district.loc[len(coffe_nearest_district.index)] = \
['Среди всех кофеен', coffe_data.nearest_coffe_m.mean().round(), coffe_data.nearest_coffe_m.median()]
coffe_nearest_district.sort_values('mean', ascending=False)
| district | mean | median | |
|---|---|---|---|
| 4 | Северо-Западный административный округ | 391.0 | 244.0 |
| 6 | Юго-Восточный административный округ | 335.0 | 191.0 |
| 0 | Восточный административный округ | 288.0 | 230.0 |
| 8 | Южный административный округ | 281.0 | 159.0 |
| 7 | Юго-Западный административный округ | 280.0 | 216.0 |
| 3 | Северо-Восточный административный округ | 248.0 | 185.0 |
| 9 | Среди всех кофеен | 232.0 | 151.0 |
| 1 | Западный административный округ | 228.0 | 138.0 |
| 2 | Северный административный округ | 223.0 | 133.0 |
| 5 | Центральный административный округ | 148.0 | 121.0 |
Промежуточный вывод
Посмотрим, со сколькими кофейнями соседствует каждая кофейня в радиусе 100м, 250м и 500м по округам.
x = pd.pivot_table(coffe_data, index='district', values=['near_coffe_100', 'near_coffe_250', 'near_coffe_500'],
aggfunc=['mean']).reset_index()
x.loc[len(x.index)] = ['Все кофейни', coffe_data.near_coffe_100.mean(), coffe_data.near_coffe_250.mean(), coffe_data.near_coffe_500.mean()]
x.round(1).sort_values([('mean', 'near_coffe_500'), ('mean', 'near_coffe_250')], ascending=False)
| district | mean | |||
|---|---|---|---|---|
| near_coffe_100 | near_coffe_250 | near_coffe_500 | ||
| 5 | Центральный административный округ | 0.7 | 2.6 | 7.8 |
| 2 | Северный административный округ | 1.0 | 2.5 | 5.2 |
| 9 | Все кофейни | 0.8 | 1.9 | 4.7 |
| 1 | Западный административный округ | 1.2 | 2.1 | 3.6 |
| 3 | Северо-Восточный административный округ | 0.7 | 1.5 | 3.4 |
| 8 | Южный административный округ | 0.8 | 1.6 | 2.9 |
| 4 | Северо-Западный административный округ | 0.4 | 1.4 | 2.8 |
| 0 | Восточный административный округ | 0.4 | 1.1 | 2.6 |
| 6 | Юго-Восточный административный округ | 0.6 | 1.1 | 2.2 |
| 7 | Юго-Западный административный округ | 0.4 | 0.8 | 2.0 |
Промежуточный вывод В ЦАО, ЗАО, СВАО и САО кофейни соседствуют с самым большим количеством кофеен в радиусе 100м, 250м и 500м. Это может свидетельствовать о том, что там присутвуют районы интереса, где на небольшом расстояния могут быть собраны сразу несколько кофеен.
Создадим карту-хитмеп, где отобразим кол-во кофеен на примерно 1 км².
#Создадим карту с тепловой картой медианого среднего чека и границами округов
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10, tiles='Cartodb Positron')
#Создаем хроноплет, где будут показаны границы округов
choropleth = folium.Choropleth(
geo_data=state_geo,
fill_opacity=0,
line_weight=2,
line_color='grey',
highlight=True,
name='Границы округов'
).add_to(m)
#Добавляем отображение названия округа
choropleth.geojson.add_child(
folium.features.GeoJsonTooltip(['name'],labels=False)
)
#Созадим данные для хитмепа карты, где каждый квадрат будет примерно 1 на 1 км
#и посчитаем кол-во кофеен на каждый квадратик. Отдельно выделим квадратики, где более 10 кофеен
median_map_data, FeatureCollection_heatmap = map_heatmap(coffe_data, section_lat=41, section_lng=31, values='nearest_coffe_m',
aggfunc='count', name_value='total_per_1_sq', limit_min=None, limit_max=10)
# создаём хроноплет для отображения хитмепа и настраиваем его
choropleth_sq = Choropleth(
geo_data=FeatureCollection_heatmap,
data=median_map_data,
columns=['index', 'nearest_coffe_m'],
key_on='feature.properties.name',
fill_color='YlGnBu',
fill_opacity=0.8,
line_opacity=0.3,
line_weight=1,
nan_fill_color='purple',
nan_fill_opacity=0.8,
highlight=True,
legend_name='Количество кофеен на 1км²',
name='Количество кофеен на 1км²/Тепловая карта',
bins=range(1, 11),
).add_to(m)
#Добавляем отображение названия округа и медианного среднего чек при наведение на кубик хитмепа
choropleth_sq.geojson.add_child(
folium.features.GeoJsonTooltip(['district', 'total_per_1_sq'], labels=True, aliases=['Полигон в', 'Кол-во кофеен'])
)
#добавляем управление слоями
folium.LayerControl().add_to(m)
# выводим карту
m
#Посмотрим, сколько есть точек в районах, где сосредоточенно большое кол-во кофеен, например более 5 штук
coffe_data_mean_count = median_map_data.copy()
coffe_data_mean_count['district'] = coffe_data_mean_count['district'].apply(lambda x: x[0])
coffe_data_mean_count.query('nearest_coffe_m > 5 | nearest_coffe_m.isna()').groupby('district').lat.count().sort_values()
district Юго-Западный административный округ 1 Восточный административный округ 2 Северо-Западный административный округ 3 Юго-Восточный административный округ 4 Северо-Восточный административный округ 6 Южный административный округ 6 Западный административный округ 7 Северный административный округ 8 Центральный административный округ 40 Name: lat, dtype: int64
Промежуточный вывод
#Создаем переменные для постороения диаграмы
names = coffe_data['is_24/7'].value_counts().index
values = coffe_data['is_24/7'].value_counts()
#Строим круговую диаграмму
fig = px.pie(names=['Некруглосуточные кофейни', 'Круглосуточные кофейни'],
values=values, color_discrete_sequence=px.colors.qualitative.Pastel)
fig.update_traces(textposition='inside', textinfo='label+percent')
fig.update_layout(title='Соотношение круглосуточные и некруглосуточные кофеен',
width=750, height=500)
fig.show()
Промежуточный вывод Круглосуточных кофеен очень мало(~4.4%). Это означает, что спрос на них совсем невысокий, следовательно при открытие кофейни режим работы 24/7 не обязательный.
rating_coffe_data = coffe_data.groupby('district').rating.mean().round(1).reset_index()
rating_coffe_data.loc[len(rating_coffe_data.index)] = ['Все кофейни', coffe_data.rating.mean().round(1)]
# строим и настраиваем столбчатую диаграмму
fig = px.bar(rating_coffe_data.sort_values('rating'),
y='district', x='rating', text='rating', color_discrete_sequence=px.colors.qualitative.Plotly)
# оформляем график
fig.update_layout(title='График среднего рейтинга по категориям заведений',
xaxis_title='Средний рейтинг',
yaxis_title='Категория заведения', template='simple_white')
fig.update_xaxes(range=[4, 4.4], showgrid=True)
fig.show()
Промежуточный вывод
#Создадим агрегированную таблицу по районам со средней и медианной ценой чашки
coffe_group_rating = coffe_data.groupby('district').agg({'middle_coffee_cup':['mean', 'median']}).reset_index()
coffe_group_rating.columns = ['district', 'mean', 'median']
coffe_group_rating.loc[len(coffe_group_rating.index)] = ['Все кофейни', coffe_data.middle_coffee_cup.mean(), coffe_data.middle_coffee_cup.median()]
coffe_group_rating = coffe_group_rating.round(-1).sort_values('mean')
#Строим и настраиваем график
fig = go.Figure(data=[
go.Bar(name='Средняя', y=coffe_group_rating.district, x=coffe_group_rating['mean'],
orientation='h', text=coffe_group_rating['mean'],
marker_color=px.colors.qualitative.Pastel[0], base=0),
go.Bar(name='Медиана', y=coffe_group_rating.district, x=coffe_group_rating['median'],
orientation='h', text=coffe_group_rating['median'],
marker_color=px.colors.qualitative.Pastel[1], base=0)
])
fig.update_layout(barmode='group', template='simple_white', height=600,
title='Стоимость чашки капучино по округам',
xaxis_title='Стоимость чашки капучино',
yaxis_title='Округ',
legend={'title':{'text':'Цена чашки капучино'}, 'font_size':12})
fig.update_xaxes(range=[100, 190], tickvals=np.arange(100, 200, 10), showgrid=True)
fig.show()
#Посмотрим описательную статистику общую и по районам
coffe_rating_describe = coffe_data.groupby('district').middle_coffee_cup.describe().round(-1)
coffe_rating_describe.loc[len(coffe_rating_describe.index)] = coffe_data.middle_coffee_cup.describe().round(-1)
coffe_rating_describe.reset_index().replace({9:'Все кофейни'}).sort_values('mean', ascending=False)
| district | count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|---|
| 5 | Центральный административный округ | 180.0 | 190.0 | 70.0 | 60.0 | 140.0 | 190.0 | 250.0 | 330.0 |
| 7 | Юго-Западный административный округ | 40.0 | 180.0 | 50.0 | 100.0 | 130.0 | 190.0 | 230.0 | 290.0 |
| 1 | Западный административный округ | 70.0 | 160.0 | 80.0 | 60.0 | 100.0 | 150.0 | 250.0 | 300.0 |
| 2 | Северный административный округ | 90.0 | 160.0 | 60.0 | 60.0 | 100.0 | 150.0 | 200.0 | 320.0 |
| 3 | Северо-Восточный административный округ | 70.0 | 160.0 | 60.0 | 60.0 | 110.0 | 160.0 | 200.0 | 300.0 |
| 9 | Все кофейни | 640.0 | 160.0 | 70.0 | 60.0 | 100.0 | 160.0 | 220.0 | 380.0 |
| 4 | Северо-Западный административный округ | 30.0 | 150.0 | 60.0 | 60.0 | 110.0 | 140.0 | 180.0 | 270.0 |
| 8 | Южный административный округ | 60.0 | 150.0 | 60.0 | 60.0 | 100.0 | 150.0 | 190.0 | 280.0 |
| 6 | Юго-Восточный административный округ | 40.0 | 140.0 | 60.0 | 60.0 | 100.0 | 130.0 | 170.0 | 380.0 |
| 0 | Восточный административный округ | 50.0 | 130.0 | 50.0 | 60.0 | 90.0 | 130.0 | 170.0 | 260.0 |
Промежуточный вывод
Рекомендации по установке цены будут данны далее.
#Создаем переменные для постороения диаграмы
names = coffe_data.chain.value_counts().index
values = coffe_data.chain.value_counts()
#Строим круговую диаграмму
fig = px.pie(names=['Несетевые кофейни', 'Сетевые кофейни'], values=values, color_discrete_sequence=px.colors.qualitative.Pastel)
fig.update_traces(textposition='inside', textinfo='percent+label+value')
fig.update_layout(title='Соотношение сетевых и несетевых кофеен',
width=750, height=600)
fig.show()
#Посмотрим процент сетевых и несетевых заведений по районам
x = pd.pivot_table(coffe_data, index='district', columns='chain', values='name', aggfunc='count').reset_index()
x.loc[len(x.index)] = ['ddd', coffe_data.query('chain == 0').shape[0], coffe_data.query('chain == 1').shape[0]]
x['total'] = x[0] + x[1]
x['procent_0'] = x[0] / x['total'] * 100
x['procent_1'] = x[1] / x['total'] * 100
x.round().sort_values('procent_0')
| chain | district | 0 | 1 | total | procent_0 | procent_1 |
|---|---|---|---|---|---|---|
| 1 | Западный административный округ | 57 | 93 | 150 | 38.0 | 62.0 |
| 4 | Северо-Западный административный округ | 28 | 34 | 62 | 45.0 | 55.0 |
| 5 | Центральный административный округ | 207 | 221 | 428 | 48.0 | 52.0 |
| 7 | Юго-Западный административный округ | 46 | 50 | 96 | 48.0 | 52.0 |
| 9 | ddd | 693 | 720 | 1413 | 49.0 | 51.0 |
| 2 | Северный административный округ | 96 | 97 | 193 | 50.0 | 50.0 |
| 3 | Северо-Восточный административный округ | 80 | 79 | 159 | 50.0 | 50.0 |
| 8 | Южный административный округ | 65 | 66 | 131 | 50.0 | 50.0 |
| 0 | Восточный административный округ | 54 | 51 | 105 | 51.0 | 49.0 |
| 6 | Юго-Восточный административный округ | 60 | 29 | 89 | 67.0 | 33.0 |
Промежуточный вывод
При анализе кофеен мы учитываем то, что кофеен в Москве достаточно, а сам рынок кофеен уже сформирован и устоявшийся в большинстве случаев.
Распределение кофеен по районам:
Особенности расположения кофеен:
Промежуточный вывод:
Круглосуточные кофейни:
Рейтинг кофеен:
Стоимость чашки капучино:
Соотношение сетевых и несетевых кофеен:
Так как планируется открывать первую кофейню, то рекомендуем не рассматривать ЮВАО, так как там находится очень много несетевых кофеен, среди которых будет легче затираться. Если открывать кофейню в ЗАО, то она сможет привлечь больше внимания из-за большого количества однообразных сетевых кафе в этом округе.
Самые интересеные округа для открытия кофейни это ЦАО, САО и ЗАО
ЗАО — округ с большим количеством кофеен, среднее расстояние между кофейнями — 228 метров. Имеет 7 мест с большим количеством кофеен на 1 км², что свидетельствует о наличии точек интереса в этом округе. В ЗАО преобладают сетевые заведения (67%), поэтому при открытии новой уникальной кофейни у людей может возникнуть больший интерес к этой точке. При размещении заведения в ЗАО рекомендуется размещаться не далее 500 метров от ближайшей кофейни. При открытии кофейни в ЗАО стоит ориентироваться на цену 160–180 рублей.
Презентация исследования с ключевыми моментами в формате PDF: